// Copyright 2008 Senary
// See credits.txt for details.

// This file is part of Omega VII.

// Omega VII is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Omega VII is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Omega VII.  If not, see <http://www.gnu.org/licenses/>.

#include "Camera.h"
#include "Constants.h"
#include "Angle.h"
#include "Vector.h"
#include "SpaceObject.h"

using namespace std;

Camera::Camera(Vector origin)
	: _origin(origin), _target(0), _targetDistance(0.0)
{
	_minRadius = 0.0;
	_maxRadius = 10000.0;
	
	_right = Vector(0.0, 1.0, 0.0);
	_upwards = Vector(0.0, 0.0, 1.0);

	//init camera orientation
	setPosition(Vector(1.0, 0.0, 0.0));
}

Camera::~Camera(void)
{
}

//getter for angle
Angle Camera::getAngle() const
{
	Angle res(_origin - _position);
	if (_upwards.z >= 0.0) {
		res.roll = 0.0;
	} else {
		res.roll = M_PI;
	}

	return res;
}

//getter for position
Vector const& Camera::getPosition() const 
{
	return _position;
}

//getter for origin
Vector const& Camera::getOrigin() const 
{
	return _origin;
}

// getter for the target
SpaceObject* Camera::getTarget() const
{
	return _target;
}


//setter for position
void Camera::setOrigin(Vector origin) 
{
	_origin = origin;
	//update camera orientation
	setPosition(_targetPosition);
}


//setter for position, camera orientation will be updated automatically
void Camera::setPosition(Vector position) 
{
	//need to minus offset, as origin may not be 0,0,0
	double radius2 = (position-_origin).r2();

	// don't allow to get too close to the origin to avoid divide-by-zero errors
	if (radius2 < 0.0001) {
		position = _origin + Vector(1.0, 0.0, 0.0);
	}

	_targetPosition = position;

	//update camera orientation and ensure we're within the boundary
	move(0, 0, 0, 0, 0.0f);
	zoom(0.0, 0.0f);

	_position = _targetPosition;
}

void Camera::setBoundary(double minRadius, double maxRadius)
{
	if(minRadius<0.0 || maxRadius<=minRadius) return;

	_minRadius = minRadius;
	_maxRadius = maxRadius;
	//update camera orientation
	setPosition(_targetPosition);
}

// Sets the target to the specified SpaceObject and starts moving towards it
void Camera::setTarget(SpaceObject* target)
{
	_target = target;
	_targetDistance = CAMERA_TARGET_DISTANCE;
}


//translation movement
void Camera::move(int up, int right, int down, int left, float timestep)
{
	// We just adjust the target position here, the camera will
	// then move towards it automatically (during the update() function)
	
	// Moving frees the camera from being bound to a target
	if (up != 0 || right != 0 || down != 0 || left != 0) {
		_target = 0;
	}

	_targetPosition -= _origin; // offset from origin adjustment
	double radius = _targetPosition.r();

	_targetPosition += (_upwards * (up - down) + _right * (right - left))
		* CAMERA_NORMAL_SPEED * timestep;

	// don't change _targetPosition's distance from the origin
	_targetPosition *= (radius / _targetPosition.r());

	_targetPosition += _origin; // undo adjustment
}

//positive value = zoom out, negative value = zoom in
void Camera::zoom(double distance, float timestep)
{
	if (_target != 0) {
		// If we are following a target, we just adjust the distance to it.	
		_targetDistance += distance * CAMERA_ZOOM_SPEED * timestep;
		if (_targetDistance < CAMERA_TARGET_MIN_DISTANCE) {
			_targetDistance = CAMERA_TARGET_MIN_DISTANCE;
		}
	} else {
		_targetPosition -= _origin; //offset from origin adjustment

		double oldDistance = _targetPosition.r();
		double newDistance = oldDistance + distance * CAMERA_ZOOM_SPEED * timestep;

		//make sure it does not cross the boundary
		if(newDistance<_minRadius) newDistance = _minRadius;
		else if(newDistance>_maxRadius) newDistance = _maxRadius;

		_targetPosition *= (newDistance / oldDistance); 

		_targetPosition += _origin; //undo adjustment
	}
}

void Camera::update(float timestep, float totalTime) {
	if (_target != 0) {
		// Move the camera to a position where the target is nicely visible
		_targetPosition = _target->getPosition() - _origin;
		
		double oldDistance = _targetPosition.r();
		double newDistance = oldDistance + _targetDistance;
		if (newDistance > _maxRadius) {
			_targetDistance -= (newDistance - _maxRadius);
			newDistance = _maxRadius;
		}
		_targetPosition *= (newDistance / oldDistance);

		// add a slight offset so the target does not block the sun
		_targetPosition += (_upwards - _right) * 0.2 * _targetDistance;
		_targetPosition += _origin;
	}

	// Move the camera's position towards the target position
	// Interpolate the zoom linearly... we want to move around
	// the sun and not through it.
	Vector direction = _targetPosition - _position;
	double oldDistance = direction.r();
	if (oldDistance < CAMERA_MAX_SPEED * timestep) {
		_position = _targetPosition;
	} else {

		// first change the position without changing the zoom
		double oldZoom = (_position - _origin).r();
		direction -= direction.getProj(_origin - _position);
		if (direction.r2() > 0.0001) {
			direction *= (CAMERA_MAX_SPEED * timestep / direction.r());
		} else {
			// Moving exactly towards centre... have to avoid division by zero.
			direction = Vector();
		}
		_position += direction;
		_position *= (oldZoom / _position.r());

		// then zoom towards the target
		double targetZoom = (_targetPosition - _origin).r();
		double newZoom;
		if (fabs(targetZoom - oldZoom) < CAMERA_ZOOM_SPEED * timestep) {
			newZoom = targetZoom;
		} else if (targetZoom > oldZoom) {
			newZoom = oldZoom + CAMERA_ZOOM_SPEED * timestep;
		} else {
			newZoom = oldZoom - CAMERA_ZOOM_SPEED * timestep;
		}

		_position *= (newZoom / oldZoom);
	}

	// Update the upwards and right vector to maintain an
	// orthonormal coordinate system
	// We also make sure up/down remains up/down...
	Vector forwards = _origin - _position;
	_right = cross(forwards, _upwards);
	_right.z = 0.0;
	_right /= _right.r();
	_upwards = cross(_right, forwards);
	_upwards /= _upwards.r();
}